<?php

namespace app\api\service;

use think\Controller;
use think\Request;

/**
 * @author 01
 * @email 319204520@qq.com
 * @date 2023-02-12 21:24
 */
class Uploader extends Controller
{
    private static $tmpDir = APP_PATH; //分片临时文件目录
    private static $saveDir = APP_PATH; //文件最终保存目录
    private static $mysql = null;
    public $fileInfo = [
        'identifier' => '',  //文件的唯一标识
        'chunkNumber' => 1, //当前是第几个分片
        'totalChunks' => 1,  //总分片数
        'filename' => '',  //文件名称
        'totalSize' => 0  //文件总大小
    ];

    function __construct(Request $request = null)
    {
        parent::__construct($request);
        $this->init();
    }

    function init()
    {
        self::$saveDir = dirname(APP_PATH) . '/public/uploads';
        if (!is_dir(self::$saveDir)) {
            @mkdir(self::$saveDir);
        }
        self::$tmpDir = dirname(APP_PATH) . '/public/temp_uploads';
        if (!is_dir(self::$tmpDir)) {
            @mkdir(self::$tmpDir);
        }
    }

    //上传分片
    public function upload()
    {
        if (!empty($_FILES)) {
            $in = @fopen($_FILES["file"]["tmp_name"], "rb");
            if (!$in = @fopen($_FILES["file"]["tmp_name"], "rb")) {
                return $this->message(1002, '打开临时文件失败');
            }
        } else {
            if (!$in = @fopen("php://input", "rb")) {
                return $this->message(1003, '打开输入流失败');
            }
        }

        $file_md5 = md5(microtime());
        if ($this->fileInfo['totalChunks'] === 1) {
            //如果只有1片，则不需要合并，直接将临时文件转存到保存目录下
            $filename = $this->fileInfo['filename'];
            $ext = explode('.', $this->fileInfo['filename']);

            $saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date('Y-m-d');
            if (!is_dir($saveDir)) {
                @mkdir($saveDir);
            }
            $uploadPath = $saveDir . DIRECTORY_SEPARATOR . $file_md5 . '.' . $ext[1];
            $res['merge'] = false;
            $res['update'] = [
                'file_path' => DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . date('Y-m-d') . DIRECTORY_SEPARATOR . $file_md5 . '.' . $ext[1],
              ];
        } else { //需要合并
            $filePath = self::$tmpDir . DIRECTORY_SEPARATOR; //临时分片文件路径
            $uploadPath = $filePath . $this->fileInfo['identifier'] . '_' . $this->fileInfo['chunkNumber']; //临时分片文件名
            $res['merge'] = true;
        }
        if (!$out = @fopen($uploadPath, "wb")) {
            return $this->message(1004, '文件不可写');
        }
        while ($buff = fread($in, 4096)) {
            fwrite($out, $buff);
        }
        @fclose($in);
        @fclose($out);

        $res['code'] = 1;
        return $res;
    }


    //检测断点和md5
    public function checkFile()
    {
        $identifier = $this->fileInfo['identifier'];
        $filePath = self::$tmpDir . DIRECTORY_SEPARATOR . $identifier; //临时分片文件路径
        $totalChunks = $this->fileInfo['totalChunks'];

        //检测文件md5是否已经存在
        $rs = $this->checkMd5($identifier, $this->fileInfo['totalSize']);
        if ($rs['isExist'] === true) {
            return $rs;
        }

        //检查分片是否存在
        $chunkExists = [];
        for ($index = 1; $index <= $totalChunks; $index++) {
            if (file_exists("{$filePath}_{$index}")) {
                array_push($chunkExists, $index);
            }
        }
        if (count($chunkExists) == $totalChunks) { //全部分片存在，则直接合成
            $this->merge();
        } else {
            $res['uploaded'] = $chunkExists;
            return $res;
        }
    }


    //检测md5表是否已存在该文件
    private function checkMd5($md5, $filesize)
    {
        $db = self::mysql();
        $sql = "SELECT count(*) as t,filepath FROM `hw_file` WHERE md5=:md5 AND filesize=:filesize";
        $stmt = $db->prepare($sql);
        $stmt->execute([
            ':md5' => $md5,
            ':filesize' => $filesize
        ]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        $count = $row['t'];
        if ($count > 0) {
            $res['isExist'] = true;
            $res['filepath'] = $row['filepath'];
        } else {
            $res['isExist'] = false;
        }
        return $res;
    }

    //合并文件
    public function merge()
    {
        $filePath = self::$tmpDir . DIRECTORY_SEPARATOR . $this->fileInfo['identifier'];
        $totalChunks = $this->fileInfo['totalChunks']; //总分片数
        $filename = $this->fileInfo['filename']; //文件名

        $done = true;
        //检查所有分片是否都存在
        $totalChunks = 10000;
        for ($index = 1; $index <= $totalChunks; $index++) {
            if (!file_exists("{$filePath}_{$index}")) {
//                $done = false;
                break;
            }
        }
        if ($done === false) {
            return $this->message(1005, '分片信息错误');
        }
        //如果所有文件分片都上传完毕，开始合并
        $timeStart = $this->getmicrotime(); //合并开始时间
        $saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date('Y-m-d');
        if (!is_dir($saveDir)) {
            @mkdir($saveDir);
        }
        $ext = explode('.', $filename);

        $media_name = md5(microtime()) . '.' . $ext[1];
        $uploadPath = $saveDir . DIRECTORY_SEPARATOR . $media_name;

        if (!$out = @fopen($uploadPath, "wb")) {
            return $this->message(1004, '文件不可写');
        }
        if (flock($out, LOCK_EX)) { // 进行排他型锁定
            for ($index = 1; $index <= $totalChunks; $index++) {
                if (!$in = @fopen("{$filePath}_{$index}", "rb")) {
                    break;
                }
                while ($buff = fread($in, 4096)) {
                    fwrite($out, $buff);
                }
                @fclose($in);
                @unlink("{$filePath}_{$index}"); //删除分片
            }

            flock($out, LOCK_UN); // 释放锁定
        }
        @fclose($out);
        $timeEnd = $this->getmicrotime(); //合并完成时间

        $res['code'] = 1;
        $res['time'] = $timeEnd - $timeStart; //合并总耗时
        $res['update'] = [
            'file_path' => DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . date('Y-m-d') . DIRECTORY_SEPARATOR . $media_name,
        ];
        return $res;

    }

    //计算时间
    private function getmicrotime()
    {
        list($usec, $sec) = explode(" ", microtime());
        return ((float)$usec + (float)$sec);
    }

    function getFilesize($num)
    {
        $p = 0;
        $format = 'B';
        if ($num > 0 && $num < 1024) {
            $p = 0;
            return number_format($num) . ' ' . $format;
        }
        if ($num >= 1024 && $num < pow(1024, 2)) {
            $p = 1;
            $format = 'KB';
        }
        if ($num >= pow(1024, 2) && $num < pow(1024, 3)) {
            $p = 2;
            $format = 'MB';
        }
        if ($num >= pow(1024, 3) && $num < pow(1024, 4)) {
            $p = 3;
            $format = 'GB';
        }
        if ($num >= pow(1024, 4) && $num < pow(1024, 5)) {
            $p = 3;
            $format = 'TB';
        }
        $num /= pow(1024, $p);
        return number_format($num, 2) . '' . $format;
    }

    //返回提示消息
    private function message($code, $msg)
    {
        $res = [
            'code' => 0,
            'message' => $msg
        ];
        return $res;
    }

    function getFileExt($file_path = '')
    {
        $ext = "";
        if ($file_path) {
            $arr = explode('.', $file_path);
            $ext = $arr[1];
        }
        return $ext;
    }


}